package org.dfl.plugin.editor.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dfl.core.data.types.Type;
import org.dfl.core.model.Place;
import org.dfl.core.model.XMLLoaderException;
import org.dfl.core.model.listeners.PlaceListener;
import org.dfl.plugin.editor.DflEditor;
import org.dfl.plugin.runner.FlowRunner;
import org.eclipse.core.resources.IResource;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.gef.EditPart;
import org.jdom.Element;
import org.jdom.Namespace;

/**
 * editor's model element that represents a a whole model
 * @author Piotr Wlodarczyk
 *
 */
public class DflDiagram extends ModelElement implements PlaceListener{
	private static Logger logger = Logger.getLogger(DflDiagram.class);
	
	public static final String CHILD_ADDED_PROP = "DflModel.child_added";
	public static final String CHILD_REMOVED_PROP = "DflModel.child_removed";
	
	private String desctiption = "";
	
	private Set<PlaceModelElement> places = new HashSet<PlaceModelElement>();
	private Set<TransitionModelElement> transitions = new HashSet<TransitionModelElement>();
	private Set<DiagramListener> listeners = new HashSet<DiagramListener>();
	
	private InputPlaceModelElement input = null;
	private OutputPlaceModelElement output = null;
	
	// set editable to false when running
	private boolean editable = true;
	private IResource diagramResource;
	private DflEditor editor; 
	private Boolean canBeRun = null;

	/**
	 * domyslny konstruktor - tylko dla nowych modeli
	 */
	public DflDiagram(){
		//trzeba go zainicjowac
		addDefaultInput();
		addDefaultOutput();
	}
	
	public DflDiagram(IResource diagramResource, Element root,  DflEditor editor) throws XMLLoaderException {
		setDiagramResource(diagramResource); // musi byc dodane przed wczytywaniem!
		this.editor = editor;
		load(root);
	}
	
	public boolean allTranisitonsWellDefined(){
		if(canBeRun == null){
			canBeRun = Boolean.TRUE;
			Set<TransitionModelElement> transitions = getTransitions();
			for (TransitionModelElement t : transitions) {
				if (!t.getTransition().areAllInputsDefined()) {
					canBeRun = Boolean.FALSE;
					return false;
				}
			}
		}
		return canBeRun;
	}

	public boolean canBeAligned(){
		return true;
	}
	
	public IResource getDiagramResource() {
		return diagramResource;
	}

	/**
	 * kazdy element modely powinien trzymac link do zrodla, do ktorego nalezy - ustawiane przy wczytywaniu
	 * modelu i przy dodawaniu nowych elementow modelu
	 * @param diagramResource
	 */
	protected void setDiagramResource(IResource diagramResource) {
		this.diagramResource = diagramResource;
	}
	
	private void addDefaultInput(){
		input = new InputPlaceModelElement();
		input.setLocation(new Point(100,100));
		places.add(input);
		input.getPlace().addListener(this);
	}
	
	private void addDefaultOutput(){
		output = new OutputPlaceModelElement();
		output.setLocation(new Point(100,300));
		places.add(output);
		output.getPlace().addListener(this);
	}

	/**
	 * Add a shape to this diagram.
	 * 
	 * @param s a non-null shape instance
	 * @return true, if the shape was added, false otherwise
	 */
	public boolean addChild(ModelElement s) {
		return addChild(s,false);
	}
	
	/**
	 * Dodaje miejsce/tranzycje do modelu - 2 modele zachowan - dodawanie zwykle/dodawanie podczas load'a.
	 * Dodawanie podczas loada rozni sie tym, ze mozna dodac do modelu wejscie/wyjscie i nie jest odpalana
	 * metoda firePropertyChange odswiezajaca za kazdym razem caly diagram
	 * 
	 * @param s dodawany elemenent modelu
	 * @param loading czy dodawanie w trybie "load" 
	 * @return
	 */
	public boolean addChild(ModelElement s, boolean loading) {
		if (s != null) {
			if(!loading) changed();
			s.setDiagram(this);
			s.resetMarkers(false);
			
			// zapisanie inputa/outputa
			if(s instanceof InputPlaceModelElement){
				if(loading){
					input = (InputPlaceModelElement) s;
					input.setDiagram(this);
					places.add(input);
					input.getPlace().addListener(this);
				}else{
					//	nie mozna tego dodawac do modelu 
					return false;
				}
			}else if(s instanceof OutputPlaceModelElement){
				if(loading){
					output = (OutputPlaceModelElement) s;
					output.setDiagram(this);
					places.add(output);
					output.getPlace().addListener(this);
				}else{
					//	nie mozna tego dodawac do modelu 
					return false;
				}
			}else if(s instanceof PlaceModelElement){
				((Shape) s).setDiagram(this);
				places.add((PlaceModelElement) s);
				((PlaceModelElement) s).getPlace().addListener(this);
			}else if(s instanceof TransitionModelElement){
				((Shape) s).setDiagram(this);
				TransitionModelElement t = (TransitionModelElement) s;
				transitions.add(t);
//				t.getTransition().startBackgroundRunning();
			}else{
				// nie znany element
				return false;
			}
			
			if(!loading) firePropertyChange(CHILD_ADDED_PROP, null, s);
			return true;
		}
		return false;
	}

	/**
	 * Return a List of Shapes in this diagram. The returned List should not be
	 * modified.
	 */
	public List<Shape> getChildren() {
		List<Shape> res = new ArrayList<Shape>(places);
		res.addAll(transitions);
		return res;
	}

	/**
	 * Remove a shape from this diagram.
	 * 
	 * @param s
	 *            a non-null shape instance;
	 * @return true, if the shape was removed, false otherwise
	 */
	public boolean removeChild(ModelElement s) {
		if (s != null && (places.remove(s) || transitions.remove(s))) {
			changed();
			s.resetMarkers(true); 
			
			// WARNING! remember the diagram for undo operation - donot set diagram null!

			firePropertyChange(CHILD_REMOVED_PROP, null, s);
			return true;
		}
		return false;
	}
	
	
	@SuppressWarnings("unchecked")
	public void load(Element root) throws XMLLoaderException {
		try {
			Namespace namespace = root.getNamespace();
			
			Element netElement = root.getChild("net");
			String inputPlaceId = netElement.getChild("toolspecific").getChildText("inputPlace", namespace);
			String outputPlaceId = netElement.getChild("toolspecific").getChildText("outputPlace", namespace);
			
			setDesctiption(netElement.getChildText("description"));
			
			Map<String,Shape> shapesMap = new HashMap<String,Shape>();
			
			// dodawanie miejsc
			List<Element> places = netElement.getChildren("place");
			for(Element pElem: places){
				String id = pElem.getAttributeValue("id",namespace);
				
				PlaceModelElement p = null;
				if(id.equals(inputPlaceId)){
					p = new InputPlaceModelElement(pElem);
				}else if(id.equals(outputPlaceId)){
					p = new OutputPlaceModelElement(pElem);
				}else{
					p = new PlaceModelElement(pElem);
				}
				addChild(p,true);
				shapesMap.put(p.getId(), p);
			}
			//dodawanie wejscia/wyjscia jesli nie ma 
			if(input == null)addDefaultInput();
			if(output == null)addDefaultOutput();
			
			//	dodawanie tranzycji
			List<Element> transitions = netElement.getChildren("transition");
			for(Element pElem: transitions){
				try{
					TransitionModelElement t  = new TransitionModelElement(pElem,this);
					addChild(t,true);
					shapesMap.put(t.getId(), t);
				}catch(Exception e){
					logger.error(e.getMessage(),e);
				}
			}
			
			// add edges
			List<Element> edges = netElement.getChildren("arc");
			for(Element pElem: edges){
				try{
					if(pElem.getChild("toolspecific")!=null && pElem.getChild("toolspecific").getChild("expand") != null){
						new NestedConnectionModelElement(pElem,shapesMap);
					}else{
						new ConnectionModelElement(pElem,shapesMap);
					}
				}catch (Exception e) {
					logger.error(e);
				}
			}
		} catch (Exception e) {
			throw new XMLLoaderException(e);
			// maybe the document is not well-formed, or it's not valid
			// or some other reasons.
		}
	}

	@SuppressWarnings("unchecked")
	public Element save() {
		Element root = new Element("pnml");
		Element net = new Element("net");
		net.setAttribute("type","");
		net.setAttribute("id","pn");
		root.addContent(net);
		
		Element desc = new Element("description");
		desc.addContent(getDesctiption());
		net.addContent(desc);
		
		for(ModelElement el : places){
			net.addContent(el.save());
			//zapisujemy wszystkie krawedzie, ktore wychodza z danego miejsca
			if(el instanceof Shape){
				Shape shape = (Shape) el;
				List<ConnectionModelElement> edges = new ArrayList<ConnectionModelElement>();
				edges.addAll(shape.getSourceConnections());
				edges.addAll(shape.getTargetConnections());
				for(ConnectionModelElement edge : edges){
					net.addContent(edge.save());
				}
			}
		}
		
		for(ModelElement el : transitions){
			net.addContent(el.save());
		}
		
		// zapisanie id wejscia i wyjscia
		Element tsElem = new Element("toolspecific");
		tsElem.setAttribute("tool","");
		tsElem.setAttribute("version","");
		net.addContent(tsElem);
		Element in = new Element("inputPlace");
		in.setText(input.getId());
		tsElem.addContent(in);
		Element out = new Element("outputPlace");
		out.setText(output.getId());
		tsElem.addContent(out);
		
		return root;
	}

	public InputPlaceModelElement getInput() {
		return input;
	}

	public OutputPlaceModelElement getOutput() {
		return output;
	}

	public Type getInputType(){
		return input.getPlace().getTypeContainer().getType();
	}
	
	public Type getOutputType(){
		return output.getPlace().getTypeContainer().getType();
	}

	public Set<TransitionModelElement> getTransitions() {
		return transitions;
	}

	public Set<PlaceModelElement> getPlaces() {
		return places;
	}

	public boolean isEditable() {
		return editable;
	}

	public DflEditor getEditor() {
		return editor;
	}

	public void setEditor(DflEditor editor) {
		this.editor = editor;
	}
	
	public void focus(ModelElement el){
		EditPart ed = (EditPart) getEditor().getViewer().getEditPartRegistry().get(el);
		getEditor().getViewer().deselectAll();
		getEditor().getViewer().appendSelection(ed);
		getEditor().getViewer().reveal(ed);
	}
	
	
	public void setEditable(boolean editable) {
		this.editable = editable;
		for(DiagramListener l : listeners) l.diagramEnabled(this,editable);
	}
	
	private void clearPlaces(){
		for(PlaceModelElement pelem : places){
			pelem.getPlace().clearData();
		}
	}
	
	public void clearData(){
		clearPlaces();
		
		if(isRunnning()){
			runner.stopTransitionExecution();
			clearPlaces();
		}
		
		for(TransitionModelElement t : transitions){
			t.runningFinished();
		}
	}
	
	public String getProjectPath(){
		return getDiagramResource().getProject().getLocation().toFile().getAbsolutePath();
	}
	
	/**************** RUNNING **************************/
	FlowRunner runner = null;
	
	public boolean isRunnning() {
		return (runner!=null && runner.isRunning());
	}
	
	public boolean isPaused() {
		return (runner!=null && runner.isPaused());	
	}
	
	public boolean isReadyToRun(){
		return (runner!=null && runner.isReadyToRun());
	}
	
	public boolean isRunningTransition(){
		return (runner!=null && runner.isRunningTransition());
	}
	
	/**
	 * run the flow - new thread is created to run a flow
	 * @param diagram
	 * @param inputData
	 */
	public void run(){
		runner =  new FlowRunner(this,listeners);
		runner.setRunning(true);
		runner.setPaused(true);
		runner.start();
	}
	
	public void pause(){
		if(runner!=null){
			runner.setPaused(true);
			runner.setStepMode(false, false);
		}
	}
	
	public void resumeOnceStep(boolean disableTokens){
		if(runner!=null){
			runner.setStepMode(true,disableTokens);
		}
	}
	
	public void resume(){
		if(runner!=null){
			runner.setPaused(false);
		}
	}
	
	@SuppressWarnings("deprecation")
	public void stop(){
		if(runner!=null){
			runner.finish();
		}
	}
	
	/***************** LISTENERS **********************/
	public void changed(){
		//reset can be run 
		canBeRun = null;
		// fire listeners
		for(DiagramListener l : listeners) l.diagramChanged(this);
	}
	
	public void addListener(DiagramListener listener) {
		this.listeners.add(listener);
		if(runner!=null){
			runner.addListener(listener);
		}
	}
	
	public void removeListener(DiagramListener listener) {
		this.listeners.remove(listener);
		if(runner!=null){
			runner.removeListener(listener);
		}
	}

	public void tokenRemoved(Place place, int historiesLeft) {
		for(DiagramListener l : listeners) l.diagramHasDataToRun(this,isReadyToRun());
	}

	public void tokenAdded(Place place) {
		for(DiagramListener l : listeners) l.diagramHasDataToRun(this,isReadyToRun());
	}

	public String getDesctiption() {
		return desctiption==null?"":desctiption;
	}

	public void setDesctiption(String desctiption) {
		this.desctiption = desctiption;
	}
}
